﻿/* 
 * Copyright (C) 2011 by Zachary J. Gramana
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
using System.Text;
using System.Windows.Forms;
using SharpPad.Forms;
using SharpPad.Properties;
using SharpScript;
using System.Yaml.Serialization;
using System.Linq;

namespace SharpPad
{
    public partial class MainForm : Form
    {
        #region Instance Fields
        // ####################################################################
        // ###### Instance Fields
        // ####################################################################
        private Boolean _isRunning;
        
        #endregion

        #region Properties
        // ####################################################################
        // ###### Properties
        // ####################################################################
        public  List<String>                AdditionalAssemblies { get; set; }
        public  List<String>                AdditionalNamespaces { get; set; }
        public  Dictionary<String, String>  AssemblyCache { get; set; }
        public  String[]                    LaunchParameters { get; set; }
        #endregion

        #region Constructors
        // ####################################################################
        // ###### Constructors
        // ####################################################################
        public MainForm()
        {
            var items = AppDomain.CurrentDomain.SetupInformation.ActivationArguments;
            LaunchParameters = items != null && items.ActivationData != null ? items.ActivationData : new string[0];
            InitializeComponent();
            AdditionalAssemblies = new List<String>();
            AdditionalNamespaces = new List<String>();
        }

        #endregion

        #region Instance Methods
        // ####################################################################
        // ###### Instance Methods
        // ####################################################################
        private void EnableUserInput(Boolean newState)
        {
            SourceCode.Enabled = newState;
        }
        private String GetSelectedCode()
        {
            var selectionStart = SourceCode.Selection.Start;
            var selectionEnd = SourceCode.Selection.End;
            var selectionLength = selectionEnd - selectionStart;
            var encoding = SourceCode.Encoding;

            var selectedText = Array.CreateInstance(typeof(Byte), selectionLength);
            Array.Copy(SourceCode.RawText, selectionStart, selectedText, 0, selectionLength);

            return encoding.GetString((byte[])selectedText);
        }
        private void RunScript()
        {
            EnableUserInput(false); // Lock the UI while the script runs.
            _isRunning = true;

            AppStatus.Text = Resources.AppStatusRunning;

            // Enable script users to invoke MessageBox().
            var scriptHostConfig = new HostConfiguration
                                       {
                                           AdditionalAssemblies = new List<string>(AdditionalAssemblies),
                                           AdditionalNamespaces = new List<string>(AdditionalNamespaces),
                                           SandboxSettings  = GetDefaultSandboxSettings(),
                                           ReuseAppDomain   = ReuseAppDomainMenuItem.Checked,
                                           Debug = false
                                       };

            scriptHostConfig.AdditionalAssemblies.Add("System.Windows.Forms.dll");
            scriptHostConfig.AdditionalNamespaces.Add("System.Windows.Forms");

            var scriptHost = new ScriptHost(scriptHostConfig, this);

            if (scriptHost == null)
                throw new NullReferenceException();

            String sourceCode;

            // Allow users to selectively execute code
            // by highlighting the desired lines then hitting F5.
            if (SourceCode.Selection.Length > 0)
            {
                sourceCode = GetSelectedCode();
            }
            else
            {
                sourceCode = SourceCode.Text;
            }

            // Add our script host event handlers...
            scriptHost.CompileFailed += (s, error) =>
                                            {
                                                var text = new StringBuilder();

                                                foreach (CompilerError errorInfo in error.Results.Errors)
                                                {
                                                    text.Append(String.Format("- {0}{1}", errorInfo.ErrorText, Environment.NewLine));
                                                }

                                                AppStatus.Text = Resources.AppStatusCompileFailed;
                                                OutputConsole.Text = text.ToString();
                                            };

            scriptHost.RuntimeException += (s, error) =>
                                               {
                                                   MessageBox.Show(error.Exception.Message, Resources.MessageBoxScriptError);
                                                   AppStatus.Text = Resources.AppStatusExitedEarly;
                                               };

            // Now run.
            var result = scriptHost.Run(sourceCode);

            if (result.ResultCode >= 0)
            {
                AppStatus.Text = Resources.AppStatusDone;
                OutputConsole.Text = result.ConsoleOutput;
            }

            _isRunning = false;

            EnableUserInput(true); // Unlock the UI.
        }
        
        public SandboxInfo GetDefaultSandboxSettings()
        {
            var perms = new System.Security.PermissionSet(PermissionState.Unrestricted);
            perms.AddPermission(new FileIOPermission(PermissionState.Unrestricted));
            
            return new SandboxInfo
                   {
                       PermissionsGranted = perms,
                       ScriptRootPath = AppDomain.CurrentDomain.BaseDirectory
                   };   
        }

        #endregion

        #region Event Handlers
        // ####################################################################
        // ###### Event Handlers
        // ####################################################################
        private void MainFormKeyDown(object sender, KeyEventArgs e)
        {
            if (!_isRunning && e.KeyCode == Keys.F5)
            {
                SaveToDefault();
                RunScript();
                e.Handled = true;
            }

            //if (!_isRunning && (e.Control && e.KeyCode == Keys.K) && (e.Control && e.KeyCode == Keys.C))
            //{
            //    if (SourceCode.Selection.Length > 0)
            //    {
            //        int selectionStart = SourceCode.Selection.Start;
            //        int selectionEnd = SourceCode.Selection.End;

            //    }
            //    else
            //    {
            //        SourceCode.Lines.Current.Text.Insert(0, "//");
            //    }
            //}

            if (!_isRunning && e.Control && e.KeyCode == Keys.S)
            {
                SaveCurrentScriptToFile();
            }

            if (_isRunning && e.Control && e.KeyCode == Keys.Cancel)
            {
                _isRunning = false;
                EnableUserInput(true); // Unlock the UI.
                AppStatus.Text = Resources.AppStatusRunCancelled;
                e.Handled = true;
            }
        }

        private void MainFormClosing(object sender, FormClosingEventArgs e)
        {
            SaveToDefault();
            Debug.WriteLine(Settings.Default.DefaultScriptCode);
        }

        private void SaveToDefault()
        {
            Settings.Default.DefaultScriptCode = SourceCode.Text;
            //Settings.Default.DefaultAdditionalAssemblies = AdditionalAssemblies.Count > 0 ? CreateCSV(AdditionalAssemblies) : String.Empty;
            //Settings.Default.DefaultAdditionalNamespaces = AdditionalNamespaces.Count > 0 ? CreateCSV(AdditionalNamespaces) : String.Empty;
            Settings.Default.Save();
            SaveAssemblyCache(AssemblyCache);
        }

        private void MainFormLoad(object sender, EventArgs e)
        {
            AssemblyCache = LoadAssemblyCache() ?? new Dictionary<string, string>();

            var openFile = LaunchParameters;
            if (openFile.Length == 1)
            {
                using (var fIn = new StreamReader(new Uri(openFile[0]).LocalPath)) // First arg is sharppad.exe
                {
                    ProcessCode(fIn);
                }

            }
            else
            {
                using (var code = new StringReader(Settings.Default.DefaultScriptCode))
                {
                    ProcessCode(code);
                }
            }

        }

        private void ImportNamespaceMenuItemClick(object sender, EventArgs e)
        {
            var dialog = new ManageNamespaces(AdditionalNamespaces);
            var result = dialog.ShowDialog();
            if (result != DialogResult.OK) return;
            AdditionalNamespaces = dialog.Namespaces;
        }

        private void ReferenceAssembliesMenuItemClick(object sender, EventArgs e)
        {
            var dialog = new ManageAssemblyReferences(AdditionalAssemblies, AssemblyCache);
            var result = dialog.ShowDialog();
            if (result != DialogResult.OK) return;

            // Add the assemblies to our cache, and to the AdditionalAssemblies list.
            AssemblyCache =
                AssemblyCache.Union(
                    dialog.References
                        .ToDictionary(k => new FileInfo(k.Path).Name, v => v.Path)
                    )
                    .ToDictionary(k => k.Key,
                                  v => v.Value);

            AdditionalAssemblies = AdditionalAssemblies.Union(
                    dialog.References.ConvertAll(x => x.Path)
                ).ToList();
        }

        #endregion

        #region Script Management

        private void SaveScriptButton_Click(object sender, EventArgs e)
        {
            SaveCurrentScriptToFile();
        }

        private void SaveCurrentScriptToFile()
        {
            //Only show save dialog if we have not previously selected a location
            if (String.IsNullOrEmpty(SaveScriptDialog.FileName))
            {
                var result = SaveScriptDialog.ShowDialog();
            }

            //Check FileName again to make sure we were given one
            if (!String.IsNullOrEmpty(SaveScriptDialog.FileName))
            {
                using (var fOut = new StreamWriter(SaveScriptDialog.FileName))
                {
                    //if (AdditionalAssemblies.Count > 0)
                    //{
                    //    fOut.WriteLine(String.Format("<assemblies>{0}</assemblies>", CreateCSV(AdditionalAssemblies)));
                    //}
                    //if (AdditionalNamespaces.Count > 0)
                    //    fOut.WriteLine(String.Format("<namespaces>{0}</namespaces>", CreateCSV(AdditionalNamespaces)));
                    fOut.WriteLine(SourceCode.Text);
                }

                //foreach (var assemblyPath in AdditionalAssemblies)
                //{
                //    var fileName = new FileInfo(assemblyPath).Name;
                //    var ext = new FileInfo(assemblyPath).Extension.Length;
                //    var assemblyName = fileName.Remove((fileName.Length - ext - 1), ext);
                //    AssemblyCache[assemblyName] = assemblyPath;
                //}

                // If the user added more assemblies during their interactive session,
                // then we need to add those to the cache.
                if (AdditionalAssemblies.Count > 0)
                {
                    var newEntries = AdditionalAssemblies
                        .Where(x => new FileInfo(x).Exists)
                        .ToDictionary(k => new FileInfo(k).Name,
                                      v => v);
                    AssemblyCache = AssemblyCache.Union(newEntries).ToDictionary(k => k.Key, v => v.Value);
                }

                // Now lets persist the cache to disk...
                SaveAssemblyCache(AssemblyCache);
            }
            else
                OutputConsole.Text = "Save Failed: No target location selected";
        }

        private void LoadScriptButtonClick(object sender, EventArgs e)
        {
            LoadScriptDialog.ShowDialog();
            var fileName = LoadScriptDialog.FileName;
            if (!String.IsNullOrEmpty(fileName))
            {
                
                using (var fIn = new StreamReader(fileName))
                {
                    ProcessCode(fIn);                    
                }
                //Set save destination for convenience
                SaveScriptDialog.FileName = fileName;

            }
        }

        private void ProcessCode(TextReader fIn) 
        {
            var assembliesCSV = String.Empty;
            var namespacesCSV = String.Empty;

            //Reset SharpPad
            SourceCode.Text = String.Empty;
            AdditionalAssemblies.Clear();
            AdditionalNamespaces.Clear();

            object[] metadata;

            var previousLineInHeader = true;

            using (fIn)
            {
                OutputConsole.Text = "Reading script file..." + Environment.NewLine;

                String input;

                var allLines = new StringBuilder();
                var headerLines = new StringBuilder();
                var lineCount = 1;

                while ((input = fIn.ReadLine()) != null)
                {
                    var trimmed = input.Trim();

                    //Grab text between <assemblies> tags... meh, I should have gone a more eloquent route
                    if      (trimmed.StartsWith("<assemblies>")) { assembliesCSV = trimmed.Substring(trimmed.IndexOf(">") + 1, (trimmed.IndexOf("</") - trimmed.IndexOf(">")) - 1); }
                    else if (trimmed.StartsWith("<namespaces>")) { namespacesCSV = trimmed.Substring(trimmed.IndexOf(">") + 1, (trimmed.IndexOf("</") - trimmed.IndexOf(">")) - 1); }
                    else if (trimmed.StartsWith("//") && previousLineInHeader) { headerLines.AppendLine(trimmed.Replace("//", String.Empty)); allLines.AppendLine(input); lineCount++; }
                    else { previousLineInHeader = false; allLines.AppendLine(input); lineCount++; }
                }
                var serializer = new YamlSerializer();
                metadata = serializer.Deserialize(headerLines.ToString(), new[] { typeof(Object) });
                SourceCode.Text = allLines.ToString();
                OutputConsole.Text += String.Format("Read a total of {0} lines ({1:###,###,###,###} bytes total).{2}", lineCount, SourceCode.Text.Length, Environment.NewLine);
            }

            var namesSpaces = new List<String>();
            var assemblies = new List<String>();

            if (!String.IsNullOrEmpty(assembliesCSV))
            {
                //Parse CSV headers and add to Additional lists
                OutputConsole.Text += String.Format("{0}Loading Assemblies...{0}" , Environment.NewLine);
                foreach (var assembly in assembliesCSV.Split(','))
                {
                    if (File.Exists(assembly))
                    {
                        OutputConsole.Text += String.Format("\t\tSuccess: {0}, {1}", assembly, Environment.NewLine);
                        assemblies.Add(assembly);
                    }
                    else
                        OutputConsole.Text += String.Format("\t\tNOT FOUND: {0}, {1}", assembly, Environment.NewLine);
                }
            }

            if (!String.IsNullOrEmpty(namespacesCSV))
            {
                OutputConsole.Text += String.Format("{0}Loading Namespaces...{0}", Environment.NewLine);

                foreach (var nmspace in namespacesCSV.Split(','))
                {
                    OutputConsole.Text += String.Format("\t\t{0}{1}", nmspace , Environment.NewLine);
                    namesSpaces.Add(nmspace);
                }
            }

            if (assemblies.Count > 0)
            {
                var newEntries = assemblies
                    .Where(x => new FileInfo(x).Exists)
                    .ToDictionary(k => new FileInfo(k).Name,
                                  v => v);
                AssemblyCache = AssemblyCache.Union(newEntries).ToDictionary(k => new FileInfo(k.Value).Name, v => v.Value);
            }

            AdditionalNamespaces = AdditionalNamespaces.Union(namesSpaces).ToList();

            try
            {
                foreach (Dictionary<object, object> aitem in metadata)
                {
                    if (aitem.ContainsKey("References"))
                    {
                        var refs = (object[]) aitem["References"];
                        var cache = refs.Select((s, i) =>
                                                    {
                                                        String assemblyPath;
                                                        var key = s.ToString();
                                                        return AssemblyCache.TryGetValue(key, out assemblyPath)
                                                                   ? assemblyPath
                                                                   : key;
                                                    });
                        AdditionalAssemblies = AdditionalAssemblies.Union(cache).ToList();
                    }

                    if (aitem.ContainsKey("Imports"))
                    {
                        var names = (object[])aitem["Imports"];
                        if (names.Count() > 0) AdditionalNamespaces.AddRange(names.Cast<String>());
                    }
                }

            }
            catch(Exception ex)
            {
                Trace.TraceError(ex.Message);
            }

            OutputConsole.Text += "Done." + Environment.NewLine;
        }

        #endregion

        private void SaveAssemblyCache(Dictionary<String,String> cache)
        {
            var serializer = new YamlSerializer();
            var settingsFile = new FileInfo(Path.Combine(Application.CommonAppDataPath, "AssemblyCache.config"));
            StreamWriter writer = null;
            try
            {
                if (settingsFile.Exists)
                {
                    writer = new StreamWriter(settingsFile.FullName, false);
                }
                else
                {
                    writer = settingsFile.CreateText();
                }

                serializer.Serialize(writer, cache);
            }
            finally
            {
                if (writer != null) writer.Close();
            }

            return;
        }

        private Dictionary<String, String> LoadAssemblyCache()
        {
            var serializer = new YamlSerializer();
            var settingsFile = new FileInfo(Path.Combine(Application.CommonAppDataPath, "AssemblyCache.config"));

            var cache = new Dictionary<String, String>();
            FileStream reader = null;

            try
            {
                if (settingsFile.Exists)
                {
                    reader = settingsFile.OpenRead();
                    cache = serializer.Deserialize(reader, typeof(Dictionary<String, String>))[0] as Dictionary<String, String>;
                }

            }
            catch(Exception ex)
            {
                Trace.TraceError(ex.Message);
            }
            finally
            {
                if (reader != null) reader.Close();
            } 
            
            return cache;
        }

    }
}
